#!/usr/bin/env perl -w

use strict;
use warnings;
use 5.8.1;
our $VERSION = '0.2';

#use Net::xFTP;
use Net::FTP::AutoReconnect;
use File::Find;
use Digest::MD5::File qw(file_md5_hex);
use XML::Dumper;
use Env qw(HOME);
use Encode;
use Pod::Usage;

#OBJECTS
my $dump;
my $ftp;

#These are set by loading a configuration file. see sub loadConfigFile
my $server;
my $password;
my $user;
my $local_dir;
my $remote_dir;
my $des;

#This Holds the fileName (root directory omitted) and the md5-sum
#of every file in the local and remote folders that we are concerned with
my $localData;
my $remoteData;

#check what the server supports
my $haveCHMOD = 0;

#THIS SHOULD BE THE SAME AS IN ftp_sync_config.pl
my $configDir = $HOME . '/.perliwebftpuploader';

#This will be the name of the file that stores the md5-sums and file-path data
#It's name is create by ftp_sync_config.pl.
my $dumpFile;

# Location of the configuration File
my $configFile = $configDir . '/config.xml';


sub loadConfigFile 
{
#	des = the description used in the saved setting;
	my $des = shift;
	my $hash;
	
  if ( $des )
  {
	if ( -s $configFile )
	{
		$hash = $dump->xml2perl($configFile);
	
		foreach my $h (@$hash) 
		{
			if ( $$h{'des'} eq $des )
			{
				$server 			= $$h{'server'};
				$password 			= $$h{'password'};
				$user 				= $$h{'user'};
				$local_dir 			= $$h{'local_dir'};
				$remote_dir 		= $$h{'remote_dir'};
				$dumpFile 			= $configDir . '/data/' . $$h{'data'};
				return 1;
			}
		}
	}
   }
	return 0;
}



		#Yes I'm aware that mkdir( $directory, 1) is recursive however
		#I couldn't get chmod(755, $directory) to be recursive. Thus I have the
		#following simple function
sub ftpMkdir
{
	my $directory = shift;
	my @dirs;
	my $updir;
	
	@dirs = split( '/', $directory );
	$updir = join( '/', @dirs[ 0 .. $#dirs - 1 ] );
	
	if ( $#dirs > 0 )
	{
		ftpMkdir ( $updir );
		$ftp->mkdir($directory, 1);
		if ($haveCHMOD)
		{
			$ftp->site('CHMOD', 755, $directory);
		}
	}
}

				#Recursive. Removes a Directory if it is empty
				#Goes to Parent Directory and Removes it if it is empty
sub removeDir 
{
	my $directory = shift;
	my @updir;
	my $count = $ftp->ls($directory);
	
	if( $#$count < 0 )
	{
		$ftp->rmdir($directory);
		
		@updir = split( '/', $directory );
		$directory = join( '/', @updir[ 0 .. $#updir - 1 ] );
			
		if ( $#updir > 0 )
		{
			removeDir($directory);
		}
	}
}

				#Takes a file path and deletes that from the remote server
sub removeFromServer 
{
	my $filePath = shift;
	my @updir;

	$ftp->delete($filePath);
	
	@updir = split( '/', $filePath );
	$filePath = join( '/', @updir[ 0 .. $#updir - 1 ] );
	if ( $#updir > 0 )
	{
		removeDir($filePath);
	}
}



				#Takes one argument
				# filePath == the path to the item to be uploaded
sub upload 
{
	my $filePath = shift;
	my $directory;
	my @updir;
	
	print "Uploading $filePath\n";
	
	#handle the directory
	@updir = split( '/', $filePath );
	$directory = join( '/', @updir[ 0 .. $#updir - 1 ] );
	&ftpMkdir($directory);

	#upload the file
	$ftp->delete($filePath);
	$ftp->put( $local_dir . '/' . $filePath, $filePath)
		or die 'put failed', $ftp->message;
	if ($haveCHMOD)
	{
		$ftp->site('CHMOD', 755, $filePath);
	}
}


				# Uses three outside variables
				# local_dir == the directory in which to traverse
				# dumpFile == the file in which to dump the xml data
				# localData == a reference to [empty] hash values
				# RETURNS the [full] hash values' reference
sub generateLocalDump 
{
		
  finddepth ( \&wanted, $local_dir);
  sub wanted
  {
	if (-f $_)
	{	
		my $fileName;
		my $getFile;
		
		$fileName = decode( 'UTF-8', $File::Find::name);
		$getFile = $fileName;
					
		if ( $fileName =~ m{\P{IsASCII}|.*~\..*|\.DS_Store|\.svn|CVS}  )
		{
			print 'Skipping', encode('UTF-8', $fileName), "\n";
		}else{
			$fileName =~ s{\Q$local_dir\E(.+)}{\.$1};
			$$localData{$fileName} = file_md5_hex( $getFile );
		}
    }
  }
	$dump->pl2xml( $localData, $dumpFile );
	return $localData;
}


				#This routine compares the differences in the local xml file and the remote xml file
				#this determies what needs to be deleted, and what needs to be uploaded
sub compareDifferences 
{
	my $key;
	my $value;
	my ($remote_size, $local_size);

	while (($key, $value) = each(%$localData)) 
	{
		if(! $$remoteData{$key} ){ #new file - upload it
	    	&upload($key);
	    }elsif ( $$remoteData{$key} ne $$localData{$key} ){ #changed file - upload it
	    	&upload($key);
	    	delete($$remoteData{$key});
	    }else
	    {
	    	#check if sizes are the same then upload if they aren't
	    	$remote_size = $ftp->size($key);
	    	$local_size = (stat($local_dir . '/' . $key))[7];
	    	unless( defined $remote_size && defined $local_size && $remote_size eq $local_size )
	    	{
	    		&upload($key);
	    	}
	    	delete($$remoteData{$key});
	    }
	}

	while (( $key, $value ) = each(%$remoteData))
	{
		&removeFromServer($key);		#local version of file was removed
	}
}


if ( ! -e $configDir )
{
	mkdir ( $configDir );
}

if ( ! -e $configDir . '/data' )
{
	mkdir ( $configDir . '/data');
}

#Create new xml object;
$dump = XML::Dumper->new;

#Here's the Button Pusher

#load the configurations
if ( &loadConfigFile($ARGV[0]) == 0 )
{
	my $error_des = $ARGV[0];
	if ( !$ARGV[0] )
	{
		$error_des = 'null'
	}
	print "Could not load Configuration \"$error_des\". Quitting.\n";
    pod2usage({-exitval => 2,
	       -verbose => 0});
}

#get xml data saved from last time
if( -e $dumpFile)
{
$remoteData = $dump->xml2pl($dumpFile);
}

print "Going to Sync $local_dir with ftp://$user\@$server:$remote_dir\n";

#create local XML data
&generateLocalDump();

#Establish a new connection to a remote host.
#$ftp = Net::xFTP->new('FTP', $server, Debug => 0, user => $user, password => $password)
#				or
#	die "Could not Connect to Host $server: $@";

$ftp = Net::FTP::AutoReconnect->new( $server, Debug=> 0)
	or die "Could not connect to Host $server: $@";
$ftp->login($user, $password)
	or die "Could not login as $user\n";

#change into remote root Directory
$ftp->cwd($remote_dir);

#Change mode of transfer so images are corrupted
$ftp->binary;

if ($ftp->supported('SITE CHMOD'))
{
	$haveCHMOD = 1;
}

#compare differences and upload/remove from server
&compareDifferences();

print "ftp://$user\@$server:$remote_dir is up to date\n";

#quit
$ftp->quit;


__END__

=pod

=head1 NAME

ftp_sync_config.pl - Sync a hierarchy of local files with a remote FTP repository

=head1 SYNOPSIS

ftp_sync.pl [description of configuration]


=head1 DESCRIPTION

This perl code was developed for use specifically with iWeb. 
It's not perfect at the moment but seeing as how apple didn't 
make a very convenient way to upload your iWeb documents onto
a ftp server I figured this would be helpful. 

You could also use the code to synchronize any directory with another 
directory on an ftp server. It assumes that it's being run on
Mac OS X with perl version 5.8.8

=head1 BUGS

The synchronization is not quite complete. This script does not deal
with symbolic links, and files with multibyte charecters.

=head1 AUTHORS

Justin

=head1 SEE ALSO

Perl(1).

=cut